Mestr JavaScripts iterator-protokol. Lær at gøre ethvert objekt itererbart, styr `for...of`-løkker, og implementer brugerdefineret iterationslogik for komplekse datastrukturer med praktiske eksempler.
LĂĄs op for brugerdefineret iteration i JavaScript: En dybdegĂĄende gennemgang af iterator-protokollen
Iteration er et af de mest fundamentale koncepter inden for programmering. Fra behandling af listeelementer til læsning af datastrømme arbejder vi konstant med sekvenser af information. I JavaScript har vi kraftfulde og elegante værktøjer som for...of-løkken og spread-syntaksen (...), der gør iteration over indbyggede typer som Arrays, Strings og Maps til en problemfri oplevelse.
Men har du nogensinde stoppet op og tænkt over, hvad der gør disse objekter så specielle? Hvorfor kan du skrive for (const char of "hello"), men ikke for (const prop of {a: 1, b: 2})? Svaret ligger i en kraftfuld, men ofte misforstået, funktion i ECMAScript-standarden: Iterator-protokollen.
Denne protokol er ikke kun en intern mekanisme for JavaScripts indbyggede objekter. Det er en åben standard, en kontrakt, som ethvert objekt kan adoptere. Ved at implementere denne protokol kan du lære JavaScript, hvordan man itererer over dine egne brugerdefinerede objekter, hvilket gør dem til førsteklasses borgere i sproget. Du kan låse op for den samme syntaktiske elegance som for...of for dine brugerdefinerede datastrukturer, uanset om det er et binært træ, en sammenkædet liste, en spils tursekvens eller en tidslinje over begivenheder.
I denne omfattende guide vil vi afmystificere iterator-protokollen. Vi vil bryde den ned i dens kernekomponenter, gennemgå opbygningen af brugerdefinerede iteratorer fra bunden, udforske avancerede anvendelsestilfælde som uendelige sekvenser, og til sidst opdage den moderne, forenklede tilgang ved hjælp af generator-funktioner. Ved afslutningen vil du ikke kun forstå, hvordan iteration virker bag kulisserne, men også være i stand til at skrive mere udtryksfuld, genanvendelig og idiomatisk JavaScript-kode.
Kernen i Iteration: Hvad er JavaScripts Iterator-protokol?
Først og fremmest er det afgørende at forstå, at "iterator-protokollen" ikke er en enkelt klasse, du udvider, eller en specifik funktion, du kalder. Det er et sæt regler eller konventioner, som et objekt skal følge for at blive betragtet som "itererbart" og for at producere en "iterator". Det er bedst at tænke på det som en kontrakt. Hvis dit objekt underskriver denne kontrakt, lover JavaScript-motoren, at den ved, hvordan den skal løkke over det.
Denne kontrakt er opdelt i to forskellige dele:
- Den Itererbare Protokol: Denne bestemmer, om et objekt er itererbart i første omgang.
- Iterator-protokollen: Denne definerer mekanikken for, hvordan objektet vil blive itereret over, én værdi ad gangen.
Lad os undersøge hver del af denne kontrakt i detaljer.
Den Første Halvdel af Kontrakten: Den Itererbare Protokol
Den itererbare protokol er overraskende simpel. Den har kun ét krav:
Et objekt betragtes som itererbart, hvis det har en specifik, velkendt egenskab, der leverer en metode til at hente en iterator. Denne velkendte egenskab tilgås ved hjælp af Symbol.iterator.
Så for at et objekt kan være itererbart, skal det have en metode, der er tilgængelig via nøglen [Symbol.iterator]. Når denne metode kaldes, skal den returnere et iterator-objekt (som vi vil dække i næste afsnit).
Du spørger måske, "Hvad er Symbol, og hvorfor ikke bare bruge et strengnavn som 'iterator'?" Et Symbol er en unik og uforanderlig primitiv datatype, der blev introduceret i ES6. Dets primære formål er at fungere som en unik nøgle for objektegenskaber for at forhindre utilsigtede navnekollisioner. Hvis protokollen brugte en simpel streng som 'iterator', kunne din egen kode definere en egenskab med samme navn til et andet formål, hvilket ville føre til uforudsigelige fejl. Ved at bruge Symbol.iterator garanterer sprogspecifikationen en unik, standardiseret nøgle, der ikke vil kollidere med anden kode.
Vi kan nemt verificere dette pĂĄ indbyggede itererbare objekter:
const anArray = [1, 2, 3];
const aString = "global";
const aMap = new Map();
console.log(typeof anArray[Symbol.iterator]); // "function"
console.log(typeof aString[Symbol.iterator]); // "function"
console.log(typeof aMap[Symbol.iterator]); // "function"
// A plain object is not iterable by default
const anObject = { a: 1, b: 2 };
console.log(typeof anObject[Symbol.iterator]); // "undefined"
Den Anden Halvdel af Kontrakten: Iterator-protokollen
Når et objekt har bevist, at det er itererbart ved at levere en [Symbol.iterator]()-metode, skifter fokus til det objekt, som metoden returnerer: iteratoren. Iteratoren er den virkelige arbejdshest; det er det objekt, der rent faktisk håndterer iterationsprocessen og producerer sekvensen af værdier.
Iterator-protokollen er også meget ligetil. Den har ét krav:
Et objekt er en iterator, hvis det har en metode ved navn next(). Denne next()-metode skal, nĂĄr den kaldes, returnere et objekt med to specifikke egenskaber:
done(boolean): Denne egenskab signalerer status for iterationen. Den erfalse, hvis der er flere værdier på vej i sekvensen. Den blivertrue, når iterationen er afsluttet.value(enhver type): Denne egenskab indeholder den aktuelle værdi i sekvensen. Nårdoneertrue, ervalue-egenskaben valgfri og indeholder typiskundefined.
Lad os se på en selvstændig, manuelt oprettet iterator for at se dette i praksis, helt adskilt fra ethvert itererbart objekt. Denne iterator vil simpelthen tælle fra 1 til 3.
const manualCounterIterator = {
count: 1,
next: function() {
if (this.count <= 3) {
return { value: this.count++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
// We call next() repeatedly to get each value
console.log(manualCounterIterator.next()); // { value: 1, done: false }
console.log(manualCounterIterator.next()); // { value: 2, done: false }
console.log(manualCounterIterator.next()); // { value: 3, done: false }
console.log(manualCounterIterator.next()); // { value: undefined, done: true }
console.log(manualCounterIterator.next()); // { value: undefined, done: true } - It stays done
Dette er den grundlæggende mekanik, der driver enhver for...of-løkke. Når du skriver for (const item of iterable), gør JavaScript-motoren følgende bag kulisserne:
- Den kalder
[Symbol.iterator]()-metoden pĂĄiterable-objektet for at fĂĄ en iterator. - Den kalder derefter gentagne gange
next()-metoden pĂĄ denne iterator. - For hvert returneret objekt, hvor
doneerfalse, tildeler denvaluetil din løkkevariabel (item) og udfører løkkens krop. - Når
next()returnerer et objekt, hvordoneertrue, afsluttes løkken.
Byg fra Bunden: En Praktisk Guide til Brugerdefineret Iteration
Nu hvor vi forstår teorien, lad os omsætte den til praksis. Vi vil oprette en brugerdefineret klasse kaldet Timeline. Denne klasse vil håndtere en samling af historiske begivenheder, og vores mål er at gøre den direkte itererbar, så vi kan løkke gennem begivenhederne i kronologisk rækkefølge.
Anvendelsestilfælde: En `Timeline`-klasse
Vores Timeline-klasse vil gemme begivenheder, hvor hver begivenhed er et objekt med et year og en description. Vi ønsker at kunne bruge en for...of-løkke til at iterere gennem disse begivenheder, sorteret efter år.
class Timeline {
constructor() {
this.events = [];
}
addEvent(year, description) {
this.events.push({ year, description });
}
}
const myTimeline = new Timeline();
myTimeline.addEvent(1995, "JavaScript is created");
myTimeline.addEvent(2009, "Node.js is introduced");
myTimeline.addEvent(1997, "ECMAScript standard is first published");
myTimeline.addEvent(2015, "ES6 (ECMAScript 2015) is released");
// Goal: Make the following code work
// for (const event of myTimeline) {
// console.log(`${event.year}: ${event.description}`);
// }
Trin-for-trin Implementering
For at nå vores mål skal vi implementere iterator-protokollen. Det betyder, at vi skal tilføje [Symbol.iterator]()-metoden til vores Timeline-klasse.
Denne metode skal returnere et nyt objekt – iteratoren – som vil indeholde next()-metoden og håndtere tilstanden af iterationen (f.eks. hvilken begivenhed vi er på). Det er et kritisk designprincip, at iterationstilstanden skal leve på iteratoren, ikke på det itererbare objekt selv. Dette giver mulighed for flere, uafhængige iterationer over den samme tidslinje samtidigt.
class Timeline {
constructor() {
this.events = [];
}
addEvent(year, description) {
// We'll add a simple check to ensure data integrity
if (typeof year !== 'number' || typeof description !== 'string') {
throw new Error("Invalid event data");
}
this.events.push({ year, description });
}
// Step 1: Implement the Iterable Protocol
[Symbol.iterator]() {
// Sort the events chronologically for iteration.
// We create a copy to not mutate the original array's order.
const sortedEvents = [...this.events].sort((a, b) => a.year - b.year);
let currentIndex = 0;
// Step 2: Return the iterator object
return {
// Step 3: Implement the Iterator Protocol with the next() method
next: () => { // Using an arrow function to capture `sortedEvents` and `currentIndex`
if (currentIndex < sortedEvents.length) {
// There are more events to iterate over
const currentEvent = sortedEvents[currentIndex];
currentIndex++;
return { value: currentEvent, done: false };
} else {
// We have reached the end of the events
return { value: undefined, done: true };
}
}
};
}
}
Oplev Magien: Brug af Vores Brugerdefinerede Itererbare Objekt
Med protokollen korrekt implementeret er vores Timeline-objekt nu et fuldgyldigt itererbart objekt. Det integreres problemfrit med JavaScripts iterationsbaserede sprogfunktioner. Lad os se det i aktion.
const myTimeline = new Timeline();
myTimeline.addEvent(1995, "JavaScript is created");
myTimeline.addEvent(2009, "Node.js is introduced");
myTimeline.addEvent(1997, "ECMAScript standard is first published");
myTimeline.addEvent(2015, "ES6 (ECMAScript 2015) is released");
console.log("--- Using for...of loop ---");
for (const event of myTimeline) {
console.log(`${event.year}: ${event.description}`);
}
// Output:
// 1995: JavaScript is created
// 1997: ECMAScript standard is first published
// 2009: Node.js is introduced
// 2015: ES6 (ECMAScript 2015) is released
console.log("\n--- Using spread syntax ---");
const eventsArray = [...myTimeline];
console.log(eventsArray);
// Output: An array of the event objects, sorted by year
console.log("\n--- Using Array.from() ---");
const eventsFrom = Array.from(myTimeline);
console.log(eventsFrom);
// Output: An array of the event objects, sorted by year
console.log("\n--- Using destructuring assignment ---");
const [firstEvent, secondEvent] = myTimeline;
console.log(firstEvent);
// Output: { year: 1995, description: 'JavaScript is created' }
console.log(secondEvent);
// Output: { year: 1997, description: 'ECMAScript standard is first published' }
Dette er den sande styrke ved protokollen. Ved at overholde en standardkontrakt har vi gjort vores brugerdefinerede objekt kompatibelt med et stort udvalg af eksisterende og fremtidige JavaScript-funktioner uden ekstra arbejde.
Udvid Dine Iterationsevner
Nu hvor du har mestret det grundlæggende, lad os udforske nogle mere avancerede koncepter, der giver dig endnu større kontrol og fleksibilitet.
Vigtigheden af Tilstand og Uafhængige Iteratorer
I vores Timeline-eksempel var vi meget omhyggelige med at placere tilstanden af iterationen (currentIndex og sortedEvents-kopien) inde i iterator-objektet, der returneres af [Symbol.iterator](). Hvorfor er dette så vigtigt? Fordi det sikrer, at hver gang vi starter en iteration, får vi en *ny, uafhængig iterator*.
Dette giver flere forbrugere mulighed for at iterere over det samme itererbare objekt uden at forstyrre hinanden. Forestil dig, hvis currentIndex var en egenskab på selve Timeline-instansen – det ville være kaos!
const sharedTimeline = new Timeline();
sharedTimeline.addEvent(1, 'Event A');
sharedTimeline.addEvent(2, 'Event B');
sharedTimeline.addEvent(3, 'Event C');
const iterator1 = sharedTimeline[Symbol.iterator]();
const iterator2 = sharedTimeline[Symbol.iterator]();
console.log(iterator1.next().value); // { year: 1, description: 'Event A' }
console.log(iterator2.next().value); // { year: 1, description: 'Event A' } (Starts its own iteration)
console.log(iterator1.next().value); // { year: 2, description: 'Event B' } (Unaffected by iterator2)
Gå Uendeligt: Oprettelse af Endeløse Sekvenser
Iterator-protokollen kræver ikke, at en iteration nogensinde slutter. Egenskaben done kan simpelthen forblive false for evigt. Dette giver os mulighed for at modellere uendelige sekvenser, hvilket kan være utroligt nyttigt til opgaver som at generere unikke ID'er, skabe strømme af tilfældige data eller modellere matematiske sekvenser.
Lad os oprette en iterator, der genererer Fibonacci-sekvensen i det uendelige.
const fibonacciSequence = {
[Symbol.iterator]() {
let a = 0, b = 1;
return {
next() {
[a, b] = [b, a + b];
return { value: a, done: false };
}
};
}
};
// We can't use spread syntax or Array.from() here, as that would create an infinite loop and crash!
// const fibArray = [...fibonacciSequence]; // DANGER: Infinite loop!
// We must consume it carefully, providing our own termination condition.
console.log("First 10 Fibonacci numbers:");
let count = 0;
for (const number of fibonacciSequence) {
console.log(number);
count++;
if (count >= 10) {
break; // It's crucial to break out of the loop!
}
}
Valgfrie Iterator-metoder: `return()`
For mere avancerede scenarier, især dem der involverer ressourcestyring (som filhåndtag eller netværksforbindelser), kan en iterator valgfrit have en return()-metode. Denne metode kaldes automatisk af JavaScript-motoren, hvis iterationen stoppes for tidligt. Dette kan ske, hvis en `break`, `return`, `throw`-sætning afslutter en `for...of`-løkke, før den er fuldført.
Dette giver din iterator en chance for at udføre oprydningsopgaver.
function createResourceIterator() {
let resourceIsOpen = true;
console.log("Resource opened.");
let i = 0;
return {
next() {
if (i < 3) {
return { value: ++i, done: false };
} else {
console.log("Iterator finished naturally.");
resourceIsOpen = false;
console.log("Resource closed.");
return { done: true };
}
},
return() {
if (resourceIsOpen) {
console.log("Iterator terminated early. Closing resource.");
resourceIsOpen = false;
}
return { done: true }; // Must return a valid iterator result
}
};
}
console.log("--- Early exit scenario ---");
const resourceIterable = { [Symbol.iterator]: createResourceIterator };
for (const value of resourceIterable) {
console.log(`Processing value: ${value}`);
if (value > 1) {
break; // This will trigger the return() method
}
}
Bemærk: Der findes også en throw()-metode til fejlhåndtering, men den bruges primært i forbindelse med generator-funktioner, som vi vil diskutere nu.
Den Moderne Tilgang: Forenkling med Generator-funktioner
Som vi har set, kræver manuel implementering af iterator-protokollen omhyggelig tilstandsstyring og standardkode for at oprette iterator-objektet og returnere { value, done }-objekterne. Selvom det er essentielt at forstå denne proces, introducerede ES6 en meget mere elegant løsning: generator-funktioner.
En generator-funktion er en speciel slags funktion, der kan pauses og genoptages, hvilket gør det muligt for den at producere en sekvens af værdier over tid. Den forenkler oprettelsen af iteratorer enormt.
Nøgle-syntaks:
function*: Stjernen erklærer en funktion som en generator.yield: Dette nøgleord pauser generatorens eksekvering og "afgiver" en værdi. Når iteratorensnext()-metode kaldes igen, genoptages funktionen, hvor den slap.
Når du kalder en generator-funktion, udfører den ikke sin krop med det samme. I stedet returnerer den et iterator-objekt, der er fuldt ud kompatibelt med protokollen. JavaScript-motoren håndterer automatisk tilstandsmaskinen, next()-metoden og oprettelsen af { value, done }-objekterne for dig.
Refaktorering af Vores `Timeline`-eksempel
Lad os se, hvor dramatisk generator-funktioner kan forenkle vores Timeline-implementering. Logikken forbliver den samme, men koden bliver langt mere læsbar og mindre fejlbehæftet.
class Timeline {
constructor() {
this.events = [];
}
addEvent(year, description) {
this.events.push({ year, description });
}
// Refactored with a generator function!
*[Symbol.iterator]() { // The asterisk makes this a generator method
// Create a sorted copy
const sortedEvents = [...this.events].sort((a, b) => a.year - b.year);
// Loop through the sorted events
for (const event of sortedEvents) {
// yield pauses the function and returns the value
yield event;
}
// When the function finishes, the iterator is automatically marked as 'done'
}
}
// Usage is exactly the same, but the implementation is cleaner!
const myGenTimeline = new Timeline();
myGenTimeline.addEvent(2002, "The Euro currency is introduced");
myGenTimeline.addEvent(1998, "Google is founded");
for (const event of myGenTimeline) {
console.log(`${event.year}: ${event.description}`);
}
Se forskellen! Den komplekse manuelle oprettelse af iterator-objektet er væk. Tilstanden (hvilken begivenhed vi er på) styres implicit af generator-funktionens pausede tilstand. Dette er den moderne, foretrukne måde at implementere iterator-protokollen på.
Styrken ved `yield*`
Generator-funktioner har endnu en superkraft: yield* (yield star). Dette giver en generator mulighed for at delegere iterationsprocessen til et andet itererbart objekt. Det er et utroligt kraftfuldt værktøj til at sammensætte iteratorer fra flere kilder.
Forestil dig, at vi har en Project-klasse, der har flere Timeline-objekter (f.eks. en for design, en for udvikling). Vi kan gøre selve Project-klassen itererbar, og den vil problemfrit iterere over alle begivenheder fra alle dens tidslinjer i rækkefølge.
class Project {
constructor(name) {
this.name = name;
this.designTimeline = new Timeline();
this.devTimeline = new Timeline();
}
*[Symbol.iterator]() {
console.log(`Iterating through events for project: ${this.name}`);
console.log("--- Design Events ---");
yield* this.designTimeline; // Delegate to the design timeline's iterator
console.log("--- Development Events ---");
yield* this.devTimeline; // Then delegate to the dev timeline's iterator
}
}
const websiteProject = new Project("Global Website Relaunch");
websiteProject.designTimeline.addEvent(2023, "Initial wireframes created");
websiteProject.designTimeline.addEvent(2024, "Final brand guide approved");
websiteProject.devTimeline.addEvent(2024, "Backend API developed");
websiteProject.devTimeline.addEvent(2025, "Frontend deployment");
for (const event of websiteProject) {
console.log(` - ${event.year}: ${event.description}`);
}
Det Store Billede: Hvorfor Iterator-protokollen er en Hjørnesten i Moderne JavaScript
Iterator-protokollen er langt mere end en akademisk kuriositet eller en funktion for biblioteksudviklere. Det er et fundamentalt designmønster, der fremmer interoperabilitet og elegant kode. Tænk på det som en universel adapter. Ved at få dine objekter til at overholde denne standard, tilslutter du dem til et massivt økosystem af sprogfunktioner, der er designet til at arbejde med enhver datasekvens.
Listen over funktioner, der er afhængige af den itererbare protokol, er omfattende og voksende:
- Løkker:
for...of - Array-oprettelse/sammensætning: Spread-syntaksen (
[...iterable]) ogArray.from(iterable) - Datastrukturer: Konstruktørerne for
new Map(iterable),new Set(iterable),new WeakMap(iterable), ognew WeakSet(iterable)accepterer alle itererbare objekter. - Asynkrone Operationer:
Promise.all(iterable),Promise.race(iterable), ogPromise.any(iterable)opererer pĂĄ en itererbar af Promises. - Destructuring: Du kan bruge destructuring assignment med ethvert itererbart objekt:
const [first, second] = myIterable; - Nye API'er: Moderne API'er som
Intl.Segmentertil tekstsegmentering returnerer ogsĂĄ itererbare objekter.
Når du gør dine brugerdefinerede datastrukturer itererbare, aktiverer du ikke kun en `for...of`-løkke; du gør dem kompatible med hele denne kraftfulde pakke af værktøjer, hvilket sikrer, at din kode er både fremtidssikret og let for andre udviklere at bruge og forstå.
Konklusion: Dine Næste Skridt inden for Iteration
Vi har rejst fra de grundlæggende regler i de itererbare og iterator-protokoller til at bygge vores egne brugerdefinerede iteratorer, og endelig til den rene, moderne syntaks i generator-funktioner. Du har nu viden til at lære JavaScript, hvordan man gennemgår enhver datastruktur, du kan forestille dig.
At mestre denne protokol er et betydeligt skridt på din rejse som JavaScript-udvikler. Det flytter dig fra at være en forbruger af sprogets funktioner til en skaber, der kan udvide sprogets kerneegenskaber til at passe til dine specifikke behov.
Handlingsorienterede Indsigter for Globale Udviklere
- Gennemgå Din Kode: Kig efter objekter i dine nuværende projekter, der repræsenterer en datasekvens. Itererer du over dem med brugerdefinerede, ikke-standardiserede metoder som
.forEachItem()eller.getItems()? Overvej at refaktorere dem til at implementere den standardiserede iterator-protokol for bedre interoperabilitet. - Omfavn 'Laziness': Brug iteratorer, og især generatorer, til at repræsentere store eller endda uendelige datasæt. Dette giver dig mulighed for at behandle data efter behov, hvilket fører til betydelige forbedringer i hukommelseseffektivitet og ydeevne. Du beregner kun det, du har brug for, når du har brug for det.
- Prioriter Generatorer: For ethvert nyt objekt, du opretter, som skal være itererbart, bør generator-funktioner (
function*) være dit standardvalg. De er mere præcise, mindre tilbøjelige til fejl i tilstandsstyring og mere læsbare end en manuel implementering. - Tænk i Sekvenser: Begynd at se programmeringsproblemer gennem en sekvens-linse. Kan en kompleks forretningsproces, en datatransformationspipeline eller en UI-tilstandsovergang modelleres som en sekvens af trin? Hvis ja, kan en iterator være det perfekte, elegante værktøj til opgaven.
Ved at integrere iterator-protokollen i dit udviklingsværktøjssæt vil du skrive renere, mere kraftfuld og mere idiomatisk JavaScript, der vil blive forstået og værdsat af udviklere overalt i verden.